﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.CodeAnalysis;
using System.Threading;

namespace Steve
{
    public interface IDirectoryNodeMetadata
    {
        string Path { get; set; }
        bool IgnoreQueryString { get; }
    }

    [Serializable]
    public sealed class DirectoryNode<T,TMetadata> where TMetadata : IDirectoryNodeMetadata
    {
        [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
        public sealed class NavigationResult
        {
            public DirectoryNode<T, TMetadata> DirectoryMatch { get; set; }
            public Lazy<T, TMetadata> ItemMatch { get; set; }
            public bool ExactMatch { get; set; }
        }

        internal static void BuildCacheFromUrlObjects<TSource>(DirectoryNode<T, TMetadata> toBuild, IEnumerable<TSource> items, Func<TSource, TMetadata> selectMetadata, Func<TSource, Func<T>> selectNodeFactory)
        {
            foreach (var u in items)
            {
                var metadata = selectMetadata(u);
                var fullName = metadata.Path;

                //root path if its not...
                var splitStart = 1;
                if (fullName.Length == 0 || (fullName[0] != '\\' && fullName[0] != '/')) splitStart = 0;

                var split = fullName.Split(new char[] { '/', '\\' }, StringSplitOptions.None);
                var splitLength = split.Length;
                var currentDir = toBuild;

                for (var i = splitStart; i < splitLength; i++)
                {
                    var name = '/' + split[i];

                    if (i < splitLength - 1)
                    {
                        //directory
                        if (!currentDir.ChildDirectories.ContainsKey(name))
                        {
                            var newDir = new DirectoryNode<T, TMetadata>(name, currentDir);
                            currentDir.ChildDirectories.Add(name, newDir);
                            currentDir = newDir;
                        }
                        else
                        {
                            currentDir = currentDir.ChildDirectories[name];
                        }
                    }
                    else
                    {
                        //child url / rewrite
                        currentDir.Children[name] = new Lazy<T, TMetadata>(selectNodeFactory(u), metadata, LazyThreadSafetyMode.PublicationOnly);
                    }
                }
            }
        }

        //TODO: make these dictionaries immutable?
        public IDictionary<string, DirectoryNode<T, TMetadata>> ChildDirectories { get; private set; }
        public IDictionary<string, Lazy<T, TMetadata>> Children { get; private set; }
        public bool IsRoot { get; private set; }
        public string Name { get; private set; }
        public DirectoryNode<T, TMetadata> Parent { get; set; }

        public Lazy<T, TMetadata> DefaultChild
        {
            get
            {
                if (Children.ContainsKey("/")) return Children["/"];

                return null;
            }
        }

        private DirectoryNode(string name)
        {
            Name = name;

            ChildDirectories = new Dictionary<string, DirectoryNode<T, TMetadata>>(StringComparer.OrdinalIgnoreCase);
            Children = new Dictionary<string, Lazy<T, TMetadata>>(StringComparer.OrdinalIgnoreCase);
        }

        internal DirectoryNode(string name, DirectoryNode<T, TMetadata> parent)
            : this(name)
        {
            IsRoot = false;
            Parent = parent;
        }

        internal DirectoryNode(string name, bool isRoot) : this(name)
        {
            IsRoot = isRoot;
        }

        public Lazy<T,TMetadata> NavigateToItem(string path, string query)
        {
            if (string.IsNullOrEmpty(path) || path == "/") throw new ArgumentNullException("path");

            var result = NavigateToCore(path, query);

            if (result.ExactMatch) return result.ItemMatch;

            return null;
        }

        public DirectoryNode<T, TMetadata> NavigateToDirectory(string path, string query)
        {
            if (string.IsNullOrEmpty(path) || path == "/") throw new ArgumentNullException("path");

            var result = NavigateToCore(path, query);

            if (result.ExactMatch) return result.DirectoryMatch;

            return null;
        }

        internal NavigationResult NavigateToCore(string path, string query)
        {
            if (path.Length > 0 && path[0] == '/') path = path.Substring(1);
            var dirs = path.Split(new char[] { '/' }, StringSplitOptions.None);
            var dirsLength = dirs.Length;
            var dirOnly = true;
            string key;
            DirectoryNode<T, TMetadata> lastDirectory = this;

            var result = new NavigationResult();
            
            for (var i = 0; i < dirsLength; i++)
            {
                key = "/" + dirs[i];

                if (!lastDirectory.ChildDirectories.ContainsKey(key))
                {
                    if (i < dirsLength - 1)
                    {
                        //not an exact match
                        result.ExactMatch = false;
                        return result;
                    }

                    dirOnly = false;
                    break;
                }

                lastDirectory = lastDirectory.ChildDirectories[key];
            }

            result.DirectoryMatch = lastDirectory;

            if (dirOnly)
            {
                result.ExactMatch = true;
                return result;
            }

            key = "/" + dirs[dirsLength - 1];

            //test with qs
            if (!string.IsNullOrWhiteSpace(query) && lastDirectory.Children.ContainsKey(key + '?' + query))
            {
                var test = lastDirectory.Children[key + '?' + query];
                if (!test.Metadata.IgnoreQueryString)
                {
                    result.ItemMatch = test;
                    result.ExactMatch = true;
                    return result;
                }
            }

            //test without qs
            if (lastDirectory.Children.ContainsKey(key))
            {
                var test = lastDirectory.Children[key];
                if (test.Metadata.IgnoreQueryString)
                {
                    result.ItemMatch = test;
                    result.ExactMatch = true;
                    return result;
                }
            }

            result.ExactMatch = false;
            return result;
        }
    }
}
